/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.pdx.internal.json; 
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonGenerator.Feature;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.pdx.JSONFormatter;
import org.apache.geode.pdx.PdxInstance;
import org.apache.geode.pdx.internal.EnumInfo;
import org.apache.geode.pdx.internal.EnumInfo.PdxInstanceEnumInfo;

/*
 * This class converts a PdxInstance into a JSON document.
 */
public class PdxToJSON 
{
  public static boolean PDXTOJJSON_UNQUOTEFIELDNAMES = Boolean.getBoolean("pdxToJson.unQuoteFieldNames");
  private PdxInstance m_pdxInstance;
  public PdxToJSON(PdxInstance pdx)
  {
    m_pdxInstance = pdx;
  }
  
  public String getJSON()
  {
    JsonFactory jf = new JsonFactory();
   // OutputStream os = new ByteArrayOutputStream();
    HeapDataOutputStream hdos = new HeapDataOutputStream(org.apache.geode.internal.Version.CURRENT);
    try {
      JsonGenerator jg = jf.createJsonGenerator(hdos, JsonEncoding.UTF8);
      enableDisableJSONGeneratorFeature(jg);
      getJSONString(jg, m_pdxInstance);
      jg.close();
      return new String(hdos.toByteArray());
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage());
    }finally{
        hdos.close();
    }
  }
  
  public byte[] getJSONByteArray()
  {
    JsonFactory jf = new JsonFactory();
    HeapDataOutputStream hdos = new HeapDataOutputStream(org.apache.geode.internal.Version.CURRENT);
    try {
      JsonGenerator jg = jf.createJsonGenerator(hdos, JsonEncoding.UTF8);
      enableDisableJSONGeneratorFeature(jg);
      getJSONString(jg, m_pdxInstance);
      jg.close();      
      return hdos.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException(e.getMessage());
    }finally {
      hdos.close();
    }    
  }
  
  private void enableDisableJSONGeneratorFeature(JsonGenerator jg) {
    jg.enable(Feature.ESCAPE_NON_ASCII);
    jg.disable(Feature.AUTO_CLOSE_TARGET); 
    jg.setPrettyPrinter(new DefaultPrettyPrinter());
    if(PDXTOJJSON_UNQUOTEFIELDNAMES)
      jg.disable(Feature.QUOTE_FIELD_NAMES);
  }
  
  protected String convertPdxToJson(final PdxInstance pdxObj){
    return (pdxObj != null ? JSONFormatter.toJSON(pdxObj) : null);
  }
  
  private void writeValue (JsonGenerator jg, Object value, String pf) throws JsonGenerationException, IOException {
  
    if(value == null)
    {      
      jg.writeNull();
    }
    else if(value.getClass().equals(Boolean.class))
    { 
      boolean b = (Boolean)value;
      jg.writeBoolean(b);  
    }
    else if(value.getClass().equals(Byte.class))
    {  
      Byte b = (Byte)value;
      jg.writeNumber(b);  
    }
    else if(value.getClass().equals(Short.class))
    { 
      Short b = (Short)value;
      jg.writeNumber(b);  
    }
    else if(value.getClass().equals(Integer.class))
    { 
      int i = (Integer)value;
      jg.writeNumber(i);
    }
    else if(value.getClass().equals(Long.class))
    { 
      long i = (Long)value;
      jg.writeNumber(i);
    }
    else if(value.getClass().equals(BigInteger.class))
    { 
      BigInteger i = (BigInteger)value;
      jg.writeNumber(i);
    }
    else if(value.getClass().equals(Float.class))
    {
      float i = (Float)value;
      jg.writeNumber(i);
    }
    else if(value.getClass().equals(BigDecimal.class))
    {
      BigDecimal i = (BigDecimal)value;
      jg.writeNumber(i);
    }
    else if(value.getClass().equals(Double.class))
    { 
      double d = (Double)value;
      jg.writeNumber(d);
    }
    else if(value.getClass().equals(String.class))
    { 
      String s = (String)value;
      jg.writeString(s);
    }
    else if(value.getClass().isArray())
    { 
      getJSONStringFromArray(jg, value, pf);
    }
    else if(value.getClass().equals(EnumInfo.class))
    { 
      jg.writeString(value.toString());
    }
    else if(value.getClass().equals(PdxInstanceEnumInfo.class))
    { 
      jg.writeString(value.toString());
    }
    else 
    {
      if(value instanceof PdxInstance)
      {
        getJSONString(jg, (PdxInstance)value);
      }
      else if(value instanceof Collection)
      { 
        getJSONStringFromCollection(jg, (Collection<?>)value, pf);
      }
      else if (value instanceof Map){ 
        getJSONStringFromMap(jg, (Map)value, pf);
      }
      else
      {
        throw new IllegalStateException("PdxInstance returns unknwon pdxfield " + pf + " for type " + value);
      }
    }
  }  
  
  private void getJSONStringFromMap(JsonGenerator jg, Map map, String pf) throws JsonGenerationException, IOException{
    
    jg.writeStartObject();
    
    Iterator iter = (Iterator) map.entrySet().iterator();
    while(iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      
      //Iterate over Map and write key-value
      jg.writeFieldName(entry.getKey().toString()); //write Key in a Map
      writeValue(jg, entry.getValue(), pf); //write value in a Map
    }
    jg.writeEndObject();
  }
  
  private String getJSONString(JsonGenerator jg, PdxInstance pdxInstance) throws JsonGenerationException, IOException
  {
    jg.writeStartObject();
    
    List<String> pdxFields = pdxInstance.getFieldNames();
    
    for (String pf : pdxFields)
    {
      Object value = pdxInstance.getField(pf);
      jg.writeFieldName(pf);
      writeValue(jg, value, pf);
    }
    jg.writeEndObject();
    return null;
  }
  
  private void getJSONStringFromArray(JsonGenerator jg, Object value, String pf) throws JsonGenerationException, IOException {
    
    if(value.getClass().getName().equals("[Z")) 
    {
      JsonHelper.getJsonFromPrimitiveBoolArray(jg, (boolean[])value, pf);
    }
    else if(value.getClass().getName().equals("[B")) 
    {
      JsonHelper.getJsonFromPrimitiveByteArray(jg, (byte[])value, pf);
    }
    else if(value.getClass().getName().equals("[S")) 
    {
      JsonHelper.getJsonFromPrimitiveShortArray(jg, (short[])value, pf);
    }
    else if(value.getClass().getName().equals("[I")) 
    {
      JsonHelper.getJsonFromPrimitiveIntArray(jg, (int[])value, pf);
    }
    else if(value.getClass().getName().equals("[J")) 
    {
      JsonHelper.getJsonFromPrimitiveLongArray(jg, (long[])value, pf);
    }
    else if(value.getClass().getName().equals("[F"))
    {
      JsonHelper.getJsonFromPrimitiveFloatArray(jg, (float[])value, pf);
    }
    else if(value.getClass().getName().equals("[D")) 
    {
      JsonHelper.getJsonFromPrimitiveDoubleArray(jg, (double[])value, pf);
    }
    else if(value.getClass().equals(Boolean[].class))
    {
      JsonHelper.getJsonFromWrapperBoolArray(jg, (Boolean[])value, pf);
    }
    else if(value.getClass().equals(Byte[].class))
    {
      JsonHelper.getJsonFromWrapperByteArray(jg, (Byte[])value, pf);
    }
    else if(value.getClass().equals(Short[].class))
    {
      JsonHelper.getJsonFromWrapperShortArray(jg, (Short[])value, pf);
    }
    else if(value.getClass().equals(Integer[].class))
    {
      JsonHelper.getJsonFromWrapperIntArray(jg, (Integer[])value, pf);
    }
    else if(value.getClass().equals(Long[].class))
    {
      JsonHelper.getJsonFromWrapperLongArray(jg, (Long[])value, pf);
    }
    else if(value.getClass().equals(Float[].class))
    {
      JsonHelper.getJsonFromWrapperFloatArray(jg, (Float[])value, pf);
    }
    else if(value.getClass().equals(Double[].class))
    {
      JsonHelper.getJsonFromWrapperDoubleArray(jg, (Double[])value, pf);
    }
    else if(value.getClass().equals(BigInteger[].class))
    {
      JsonHelper.getJsonFromBigIntArray(jg, (BigInteger[])value, pf);
    }
    else if(value.getClass().equals(BigDecimal[].class))
    {
      JsonHelper.getJsonFromBigDecimalArray(jg, (BigDecimal[])value, pf);
    }
    else if(value.getClass().equals(String[].class))
    {
      JsonHelper.getJsonFromStringArray(jg, (String[])value, pf);
    }else if (value.getClass().equals(Object[].class)) 
    {
      jg.writeStartArray();
      Object[] array = (Object[])value;
      for (Object obj : array)
      {
        writeValue(jg, obj, pf);
      }
      jg.writeEndArray();
    } else{
      throw new IllegalStateException("PdxInstance returns unknwon pdxfield " + pf + " for type " + value);
    }
  }
  
  private <T> void  getJSONStringFromArray1(JsonGenerator jg, T[] array, String pf) throws JsonGenerationException, IOException {
    jg.writeStartArray();
    
    for (T obj : array)
    {
      writeValue(jg, obj, pf);
    }
    jg.writeEndArray();
  }
  
  
  private void getJSONStringFromCollection(JsonGenerator jg, Collection<?> coll, String pf) throws JsonGenerationException, IOException
  {
    jg.writeStartArray();
    
    for (Object obj : coll)
    {
      writeValue(jg, obj, pf);
    }
    jg.writeEndArray();    
  }
}
